/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.rebind;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;

import org.bmi.gwt.mi.client.impl.ObjectWrapperClient;
import org.bmi.gwt.mi.client.impl.ObjectWrapperClient.MethodInvoker;
import org.bmi.gwt.mi.client.impl.ParamConverter;
import org.bmi.gwt.mi.shared.MiObject;
import org.bmi.gwt.mi.shared.communication.ptypes.Param;
import org.bmi.gwt.mi.shared.communication.ptypes.array.ArrayArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.ArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.BooleanArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.ByteArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.CharacterArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.DoubleArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.FloatArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.IntegerArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.LongArrayWrapper;
import org.bmi.gwt.mi.shared.communication.ptypes.array.ShortArrayWrapper;
import org.bmi.gwt.mi.shared.exceptions.NotAvailableConstructorException;
import org.bmi.gwt.mi.shared.util.ArrayUtils;
import org.bmi.gwt.mi.shared.util.ObjectWrapper;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JAbstractMethod;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JConstructor;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.generator.NameFactory;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

class WrapperCreator
{
	/** Simple name of class to be generated */
	private String wrapperClassName = null;

	/** Package name of class to be generated */
	private String packageName = null;

	/** The type of provided class */
	private JClassType providedType;

	private String canonicalName;

	private String staticCreatorName;
	private String staticTrCreatorName;

	private HashMap<String, UnwrapArrayFunctionCreator> unwrappersMap;

	/**
	 * This class is responsible to define the name and the source code of
	 * functions unwrapping a wrapped array.
	 */
	private class UnwrapArrayFunctionCreator
	{
		private JType componentType;
		private UnwrapArrayFunctionCreator subUnwrapper;

		JArrayType arrayType;
		String methodName;

		UnwrapArrayFunctionCreator(JArrayType arrayType)
		{
			this.arrayType = arrayType;
			this.methodName = arrayType.getLeafType().getParameterizedQualifiedSourceName().replace('.', '_')
					.replace('<', '_').replace('>', '_')
					+ this.arrayType.getRank();

			this.componentType = this.arrayType.getComponentType();
			JArrayType subArrays = this.componentType.isArray();

			if (subArrays != null)
			{
				this.subUnwrapper = new UnwrapArrayFunctionCreator(subArrays);

				if (!WrapperCreator.this.unwrappersMap.containsKey(this.subUnwrapper.methodName))
					WrapperCreator.this.unwrappersMap.put(this.subUnwrapper.methodName, this.subUnwrapper);
			}
		}

		void produce(TreeLogger logger, SourceWriter w)
		{
			w.println();
			w.println("public static %s %s(Object v) {", this.arrayType.getParameterizedQualifiedSourceName(),
					this.methodName);
			w.indent();

			JPrimitiveType primCompType = this.componentType.isPrimitive();

			if (this.subUnwrapper != null)
			{
				w.println("if (v instanceof %s) {", ArrayArrayWrapper.class.getSimpleName());
				w.indent();

				JType leafType = this.arrayType.getLeafType();
				int rk = arrayType.getRank();

				StringBuilder brackets = new StringBuilder();

				for (int i = 0; i < rk - 1; i++)
					brackets.append("[]");

				w.println("%s<?> values = ((%s<?>) v).getValues();", List.class.getSimpleName(),
						ArrayArrayWrapper.class.getSimpleName());

				w.println("%s[] array = new %s[values.size()]%s;",
						this.componentType.getParameterizedQualifiedSourceName(),
						leafType.getParameterizedQualifiedSourceName(), brackets);
				w.println("int i = 0;");
				w.println();

				w.println("for (Object value : values) {");
				w.indent();
				w.println("%s<?, ?> wrapper = (%s<?, ?>) value;", ArrayWrapper.class.getSimpleName(),
						ArrayWrapper.class.getSimpleName());
				w.println();
				w.println("array[i++] = (%s)%s(wrapper);", this.componentType.getParameterizedQualifiedSourceName(),
						this.subUnwrapper.methodName);

				w.outdent();
				w.println("}");
				w.println();

				w.println("return array;");
				w.outdent();
				w.println("}");
			}
			else if (primCompType != null)
				switch (primCompType)
				{
				case BOOLEAN:
					w.println("if (v instanceof %s) return %s.asBooleanArray(((%s) v).getValues());",
							ArrayWrapper.class.getSimpleName(), ArrayUtils.class.getSimpleName(),
							BooleanArrayWrapper.class.getSimpleName());
					break;
				case BYTE:
					w.println("if (v instanceof %s) return %s.asByteArray(((%s) v).getValues());",
							ArrayWrapper.class.getSimpleName(), ArrayUtils.class.getSimpleName(),
							ByteArrayWrapper.class.getSimpleName());
					break;
				case CHAR:
					w.println("if (v instanceof %s) return %s.asCharArray(((%s) v).getValues());",
							ArrayWrapper.class.getSimpleName(), ArrayUtils.class.getSimpleName(),
							CharacterArrayWrapper.class.getSimpleName());
					break;
				case DOUBLE:
					w.println("if (v instanceof %s) return %s.asDoubleArray(((%s) v).getValues());",
							ArrayWrapper.class.getSimpleName(), ArrayUtils.class.getSimpleName(),
							DoubleArrayWrapper.class.getSimpleName());
					break;
				case FLOAT:
					w.println("if (v instanceof %s) return %s.asFloatArray(((%s) v).getValues());",
							ArrayWrapper.class.getSimpleName(), ArrayUtils.class.getSimpleName(),
							FloatArrayWrapper.class.getSimpleName());
					break;
				case INT:
					w.println("if (v instanceof %s) return %s.asIntArray(((%s) v).getValues());",
							ArrayWrapper.class.getSimpleName(), ArrayUtils.class.getSimpleName(),
							IntegerArrayWrapper.class.getSimpleName());
					break;
				case LONG:
					w.println("if (v instanceof %s) return %s.asLongArray(((%s) v).getValues());",
							ArrayWrapper.class.getSimpleName(), ArrayUtils.class.getSimpleName(),
							LongArrayWrapper.class.getSimpleName());
					break;
				case SHORT:
					w.println("if (v instanceof %s) return %s.asShortArray(((%s) v).getValues());",
							ArrayWrapper.class.getSimpleName(), ArrayUtils.class.getSimpleName(),
							ShortArrayWrapper.class.getSimpleName());
					break;
				default:
					w.println("return null;");
				}
			else
				w.println("if (v instanceof %s) return ((%s<?, ?>) v).getValues().toArray(new %s[0]);",
						ArrayWrapper.class.getSimpleName(), ArrayWrapper.class.getSimpleName(),
						this.componentType.getParameterizedQualifiedSourceName());

			w.println();
			w.println("return (%s) v;", this.arrayType.getParameterizedQualifiedSourceName());

			w.outdent();
			w.println("}");
		}
	}

	/**
	 * Standard constructor: creates a class extending
	 * {@link ObjectWrapperClient} which will wrap an element of provided type;
	 * the provided type should implement {@link MiObject}.
	 * 
	 * @param logger
	 * @param context
	 * @param providedType The provided type implementing {@link MiObject}.
	 * @throws UnableToCompleteException
	 */
	WrapperCreator(TreeLogger logger, GeneratorContext context, JClassType providedType)
			throws UnableToCompleteException
	{
		this.providedType = providedType;

		this.wrapperClassName = this.providedType.getSimpleSourceName() + "___Wrapper";
		this.packageName = this.providedType.getPackage().getName();

		this.canonicalName = this.packageName + "." + this.wrapperClassName;

		this.unwrappersMap = new HashMap<String, UnwrapArrayFunctionCreator>();

		generateClass(logger, context);

		logger.log(TreeLogger.INFO, "Generated class " + this.canonicalName);
	}

	/**
	 * Returns the name of the generated class.
	 * 
	 * @return
	 */
	String getCanonicalname()
	{
		return this.canonicalName;
	}

	/**
	 * Returns the provided type to be wrapper, implementing {@link MiObject}.
	 * 
	 * @return
	 */
	JClassType getProvideType()
	{
		return this.providedType;
	}

	/**
	 * Returns the name of the static method creating a new instance.
	 * 
	 * @return
	 */
	String getStaticCreatorMethodName()
	{
		return this.staticCreatorName;
	}

	/**
	 * Returns the name of the static method creating a new instance in
	 * transactional environment.
	 * 
	 * @return
	 */
	public String getStaticTrCreatorMethodName()
	{
		return this.staticTrCreatorName;
	}

	/**
	 * Generate source code for new class. Class extends <code>HashMap</code>.
	 * 
	 * @param logger Logger object
	 * @param context Generator context
	 */
	private void generateClass(TreeLogger logger, GeneratorContext context)
	{
		TypeOracle typeOracle = context.getTypeOracle();
		PrintWriter printWriter = context.tryCreate(logger, this.packageName, this.wrapperClassName);

		/*
		 * When the print writer is null, the source code has ALREADY been
		 * generated, in such case nothing will be done.
		 */
		if (printWriter == null) return;

		/* initialize composer, set class properties, create source writer */
		ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(this.packageName,
				this.wrapperClassName);

		composer.addImport(this.providedType.getQualifiedSourceName());
		composer.addImport(ParamConverter.class.getCanonicalName());
		composer.addImport(Param.class.getCanonicalName());
		composer.addImport(MethodInvoker.class.getCanonicalName());
		composer.addImport(NotAvailableConstructorException.class.getCanonicalName());
		composer.addImport(HashMap.class.getCanonicalName());
		composer.addImport(List.class.getCanonicalName());
		composer.addImport(ObjectWrapperClient.class.getCanonicalName());
		composer.addImport(ObjectWrapper.class.getCanonicalName());
		composer.addImport(ArrayUtils.class.getCanonicalName());
		composer.addImport(ArrayWrapper.class.getCanonicalName());
		composer.addImport(ArrayArrayWrapper.class.getCanonicalName());
		composer.addImport(BooleanArrayWrapper.class.getCanonicalName());
		composer.addImport(ByteArrayWrapper.class.getCanonicalName());
		composer.addImport(CharacterArrayWrapper.class.getCanonicalName());
		composer.addImport(DoubleArrayWrapper.class.getCanonicalName());
		composer.addImport(FloatArrayWrapper.class.getCanonicalName());
		composer.addImport(IntegerArrayWrapper.class.getCanonicalName());
		composer.addImport(LongArrayWrapper.class.getCanonicalName());
		composer.addImport(ShortArrayWrapper.class.getCanonicalName());

		composer.setSuperclass(ObjectWrapperClient.class.getSimpleName());

		SourceWriter sourceWriter = composer.createSourceWriter(context, printWriter);

		NameFactory nameFactory = new NameFactory();

		/* Generating constructor and required methods */
		generateConstructor(logger, sourceWriter, typeOracle, nameFactory);

		/*
		 * Generating a method providing an instance created by the requested
		 * constructor.
		 */
		generateStaticCreator(logger, sourceWriter, typeOracle, nameFactory);
		generateStaticTrCreator(logger, sourceWriter, typeOracle, nameFactory);

		/* Generating methods responsible of array conversion */
		for (UnwrapArrayFunctionCreator ss : this.unwrappersMap.values())
			ss.produce(logger, sourceWriter);

		/* close generated class */
		sourceWriter.outdent();
		sourceWriter.println("}");

		context.commit(logger, printWriter);
	}

	/**
	 * Generate source code for the default constructor.
	 * 
	 * @param logger
	 * @param w Source writer to output source code.
	 * @param typeOracle
	 * @param nameFactory
	 */
	private void generateConstructor(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, NameFactory nameFactory)
	{
		w.println("public " + this.wrapperClassName + "() { ");
		w.indent();
		w.println("super();");

		addMethodInvokers(logger, w, typeOracle, nameFactory);

		w.outdent();
		w.println("}");
	}

	/**
	 * Creates a static method returning an instance created using the provided
	 * constructor.
	 * 
	 * @param logger
	 * @param w
	 * @param typeOracle
	 * @param nameFactory
	 */
	private void generateStaticCreator(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, NameFactory nameFactory)
	{
		w.println("public static %s %s(String signature, %s<String, %s> objectMap, %s[] args) throws Exception { ",
				this.providedType.getSimpleSourceName(),
				(this.staticCreatorName = nameFactory.createName("___create")), HashMap.class.getSimpleName(),
				ObjectWrapper.class.getSimpleName(), Param.class.getSimpleName());
		w.indent();

		String errorMessage;

		if (this.providedType.isMemberType())
			errorMessage = "because it's an inner class.";
		else if (this.providedType.isInterface() != null)
			errorMessage = "because it's an interface.";
		else if (this.providedType.isAbstract())
			errorMessage = "because it's an abstract class.";
		else
		{
			for (JConstructor constructor : this.providedType.getConstructors())
			{
				StringBuilder _signature = new StringBuilder();

				for (JType param : constructor.getParameterTypes())
					_signature.append(param.getJNISignature());

				w.print("if (signature.equals(\"%s\")) return new %s(", _signature.toString(),
						this.providedType.getSimpleSourceName());

				JParameter[] params = constructor.getParameters();

				for (int i = 0; i < params.length; i++)
				{
					if (i > 0) w.print(", ");

					JType paramType = params[i].getType();
					JPrimitiveType primitiveType = paramType.isPrimitive();
					JArrayType paramArrayType = paramType.isArray();

					String castTo;

					if (primitiveType != null)
					{
						castTo = primitiveType.getQualifiedBoxedSourceName();
						w.print("(%s) %s.convertParameter(objectMap, args[%d].getValue())", castTo,
								ParamConverter.class.getSimpleName(), i);
					}
					else if (paramArrayType != null)
					{
						UnwrapArrayFunctionCreator unwrapCreator = new UnwrapArrayFunctionCreator(paramArrayType);
						if (!this.unwrappersMap.containsKey(unwrapCreator.methodName))
							this.unwrappersMap.put(unwrapCreator.methodName, unwrapCreator);

						castTo = paramType.getParameterizedQualifiedSourceName();

						w.print("(%s) %s(%s.convertParameter(objectMap, args[%d].getValue()))", castTo,
								unwrapCreator.methodName, ParamConverter.class.getSimpleName(), i);
					}
					else
					{
						castTo = paramType.getParameterizedQualifiedSourceName();
						w.print("(%s) %s.convertParameter(objectMap, args[%d].getValue())", castTo,
								ParamConverter.class.getSimpleName(), i);
					}
				}

				w.println(");");
			}

			errorMessage = " because no constructor matches the provided arguments. ";
		}

		/* In case no constructor is matched, an exception will be thrown */
		w.println();
		w.println("throw new %s(\"The provided type %s can't be instantiated %s\");",
				NotAvailableConstructorException.class.getSimpleName(), this.providedType.getQualifiedSourceName(),
				errorMessage);
		w.outdent();
		w.println("}");
	}

	/**
	 * Creates a static method returning an instance created using the provided
	 * constructor.
	 * 
	 * @param logger
	 * @param w
	 * @param typeOracle
	 * @param nameFactory
	 */
	private void generateStaticTrCreator(TreeLogger logger, SourceWriter w, TypeOracle typeOracle,
			NameFactory nameFactory)
	{
		w.println(
				"public static %s %s(String signature, %s<String, Object> returnValueMap, %s<String, %s> objectMap, %s[] args) throws Exception { ",
				this.providedType.getSimpleSourceName(),
				(this.staticTrCreatorName = nameFactory.createName("___createTr")), HashMap.class.getSimpleName(),
				HashMap.class.getSimpleName(), ObjectWrapper.class.getSimpleName(), Param.class.getSimpleName());
		w.indent();

		String errorMessage;

		if (this.providedType.isMemberType())
			errorMessage = "because it's an inner class.";
		else if (this.providedType.isInterface() != null)
			errorMessage = "because it's an interface.";
		else if (this.providedType.isAbstract())
			errorMessage = "because it's an abstract class.";
		else
		{
			for (JConstructor constructor : this.providedType.getConstructors())
			{
				StringBuilder _signature = new StringBuilder();

				for (JType param : constructor.getParameterTypes())
					_signature.append(param.getJNISignature());

				w.print("if (signature.equals(\"%s\")) return new %s(", _signature.toString(),
						this.providedType.getSimpleSourceName());

				JParameter[] params = constructor.getParameters();

				for (int i = 0; i < params.length; i++)
				{
					if (i > 0) w.print(", ");

					JType paramType = params[i].getType();
					JPrimitiveType primitiveType = paramType.isPrimitive();
					JArrayType paramArrayType = paramType.isArray();

					String castTo;

					if (primitiveType != null)
					{
						castTo = primitiveType.getQualifiedBoxedSourceName();
						w.print("(%s) %s.convertParameter(objectMap, args[%d].getValue())", castTo,
								ParamConverter.class.getSimpleName(), i);
					}
					else if (paramArrayType != null)
					{
						UnwrapArrayFunctionCreator unwrapCreator = new UnwrapArrayFunctionCreator(paramArrayType);
						if (!this.unwrappersMap.containsKey(unwrapCreator.methodName))
							this.unwrappersMap.put(unwrapCreator.methodName, unwrapCreator);

						castTo = paramType.getParameterizedQualifiedSourceName();

						w.print("(%s) %s(%s.convertParameter(returnValueMap, objectMap, args[%d].getValue()))", castTo,
								unwrapCreator.methodName, ParamConverter.class.getSimpleName(), i);
					}
					else
					{
						castTo = paramType.getParameterizedQualifiedSourceName();
						w.print("(%s) %s.convertParameter(returnValueMap, objectMap, args[%d].getValue())", castTo,
								ParamConverter.class.getSimpleName(), i);
					}
				}

				w.println(");");
			}

			errorMessage = " because no constructor matches the provided arguments. ";
		}

		/* In case no constructor is matched, an exception will be thrown */
		w.println();
		w.println("throw new %s(\"The provided type %s can't be instantiated %s\");",
				NotAvailableConstructorException.class.getSimpleName(), this.providedType.getQualifiedSourceName(),
				errorMessage);
		w.outdent();
		w.println("}");
	}

	protected void addMethodInvokers(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, NameFactory nameFactory)
	{
		JMethod[] methods = this.providedType.getOverridableMethods();

		for (JMethod method : methods)
			if (method.isPublic())
			{
				String methodName = method.getName();
				nameFactory.addName(methodName);

				/* We don't wrap static methods, if there are. */
				if (!method.isStatic()) addMethodInvoker(logger, w, typeOracle, (JMethod) prepareMethod(method));
			}
	}

	private JAbstractMethod prepareMethod(JAbstractMethod method)
	{
		JClassType enclosingType = method.getEnclosingType();
		JParameterizedType isParameterizedType = enclosingType.isParameterized();

		if (isParameterizedType != null)
		{
			JMethod[] _methods = isParameterizedType.getMethods();

			for (int i = 0; i < _methods.length; ++i)
				if (_methods[i] == method) return isParameterizedType.getBaseType().getMethods()[i];
		}

		return method;
	}

	private void addMethodInvoker(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, JMethod method)
	{
		w.println();

		/*
		 * Reserving the name of the method, in case that default method will
		 * not conflict with some user defined method name
		 */
		String methodName = method.getName();

		/* Extracting the arguments signature */
		JParameter[] params = method.getParameters();

		StringBuilder signature = new StringBuilder(methodName);

		for (int i = 0; i < params.length; ++i)
			signature.append(params[i].getType().getJNISignature());

		logger.log(TreeLogger.INFO, "Adding invoker for method identifier: " + signature);

		w.print("methodMap.put(\"%s\", ", signature);
		w.println("new %s(){", MethodInvoker.class.getSimpleName());
		w.indent();

		JType returnType = method.getReturnType();
		JPrimitiveType returnPrmType = returnType.isPrimitive();

		String returnTypeName;

		if (returnPrmType != null)
			returnTypeName = returnPrmType.getQualifiedBoxedSourceName();
		else
			returnTypeName = returnType.getParameterizedQualifiedSourceName();

		w.println("public %s invoke(%s<String, %s> objectMap, %s<?>[] args) throws Exception {", returnTypeName,
				HashMap.class.getSimpleName(), ObjectWrapper.class.getSimpleName(), Param.class.getSimpleName());
		w.indent();

		boolean returnsVoid = (returnPrmType == JPrimitiveType.VOID);

		if (!returnsVoid) w.print("return ");

		w.print("((%s)%s.super.instance).%s(", this.providedType.getSimpleSourceName(), this.wrapperClassName,
				method.getName());

		for (int i = 0; i < params.length; ++i)
		{
			if (i > 0) w.print(", ");

			JType paramType = params[i].getType();
			JPrimitiveType primitiveType = paramType.isPrimitive();
			JArrayType paramArrayType = paramType.isArray();

			String castTo;

			if (primitiveType != null)
			{
				castTo = primitiveType.getQualifiedBoxedSourceName();
				w.print("(%s) %s.convertParameter(objectMap, args[%d].getValue())", castTo,
						ParamConverter.class.getSimpleName(), i);
			}
			else if (paramArrayType != null)
			{
				UnwrapArrayFunctionCreator unwrapCreator = new UnwrapArrayFunctionCreator(paramArrayType);
				if (!this.unwrappersMap.containsKey(unwrapCreator.methodName))
					this.unwrappersMap.put(unwrapCreator.methodName, unwrapCreator);

				castTo = paramType.getParameterizedQualifiedSourceName();

				w.print("(%s) %s(%s.convertParameter(objectMap, args[%d].getValue()))", castTo,
						unwrapCreator.methodName, ParamConverter.class.getSimpleName(), i);
			}
			else
			{
				castTo = paramType.getParameterizedQualifiedSourceName();
				w.print("(%s) %s.convertParameter(objectMap, args[%d].getValue())", castTo,
						ParamConverter.class.getSimpleName(), i);
			}
		}

		w.println(");");

		if (returnsVoid) w.println("return null;");

		w.outdent();
		w.println("}");

		w.println();

		w.println(
				"public %s invoke(%s<String, Object> returnValueMap, %s<String, %s> objectMap, %s<?>[] args) throws Exception {",
				returnTypeName, HashMap.class.getSimpleName(), HashMap.class.getSimpleName(),
				ObjectWrapper.class.getSimpleName(), Param.class.getSimpleName());
		w.indent();

		if (!returnsVoid) w.print("return ");

		w.print("((%s)%s.super.instance).%s(", this.providedType.getSimpleSourceName(), this.wrapperClassName,
				method.getName());

		for (int i = 0; i < params.length; ++i)
		{
			if (i > 0) w.print(", ");

			JType paramType = params[i].getType();
			JPrimitiveType primitiveType = paramType.isPrimitive();
			JArrayType paramArrayType = paramType.isArray();

			String castTo;

			if (primitiveType != null)
			{
				castTo = primitiveType.getQualifiedBoxedSourceName();
				w.print("(%s) %s.convertParameter(returnValueMap, objectMap, args[%d].getValue())", castTo,
						ParamConverter.class.getSimpleName(), i);
			}
			else if (paramArrayType != null)
			{
				UnwrapArrayFunctionCreator unwrapCreator = new UnwrapArrayFunctionCreator(paramArrayType);
				if (!this.unwrappersMap.containsKey(unwrapCreator.methodName))
					this.unwrappersMap.put(unwrapCreator.methodName, unwrapCreator);

				castTo = paramType.getParameterizedQualifiedSourceName();

				w.print("(%s) %s(%s.convertParameter(returnValueMap, objectMap, args[%d].getValue()))", castTo,
						unwrapCreator.methodName, ParamConverter.class.getSimpleName(), i);
			}
			else
			{
				castTo = paramType.getParameterizedQualifiedSourceName();
				w.print("(%s) %s.convertParameter(returnValueMap, objectMap, args[%d].getValue())", castTo,
						ParamConverter.class.getSimpleName(), i);
			}
		}

		w.println(");");

		if (returnsVoid) w.println("return null;");

		w.outdent();
		w.println("}");

		w.outdent();
		w.println("});");
	}
}
